﻿using System;
using System.Net.Sockets;
using P2P.Network.ExtensionMethods;
using System.Net;
using System.Text;

namespace P2P.Network.SocketServer
{
    public delegate void DataReceivedCallback(ServerConnection sender, DataEventArgs e);

    public delegate void DisconnectedCallback(ServerConnection sender, SocketAsyncEventArgs e);

    public class ServerConnection
    {
        #region Internal Classes
        public class State
        {
            public DataReceivedCallback dataReceived;
            public DisconnectedCallback disconnectedCallback;
            public Socket socket;
        }
        #endregion

        #region Fields
        private SocketAsyncEventArgs eventArgs;
        private List<byte> receivedPartialData = new List<byte>();
        #endregion

        #region Properties
        public SocketAsyncEventArgs EventArgs 
        {
            get { return eventArgs; }
        }
        #endregion

        #region Constructor

        public ServerConnection(Socket socket, SocketAsyncEventArgs args, DataReceivedCallback dataReceived,
            DisconnectedCallback disconnectedCallback)
        {
            lock (this)
            {
                State state = new State() { socket = socket, dataReceived = dataReceived,
                    disconnectedCallback = disconnectedCallback};

                eventArgs = args;
                eventArgs.Completed += ReceivedCompleted;
                eventArgs.UserToken = state;

                ListenForData(eventArgs);
            }
        }
        #endregion

        #region Public Methods

        public void Disconnect()
        {
            lock (this)
            {
                CloseConnection(eventArgs);
            }
        }

        public void SendData(Byte[] data, Int32 offset, Int32 count)
        {
            lock (this)
            {
                State state = eventArgs.UserToken as State;
                Socket socket = state.socket;
                if (socket.Connected)
                    socket.Send(data, offset, count, SocketFlags.None);
            }
        }

        public void SendData(string data)
        {
            lock (this)
            {
                State state = eventArgs.UserToken as State;
                Socket socket = state.socket;
                if (socket.Connected)
                    socket.Send(Encoding.UTF8.GetBytes(data));
            }
        }

        #endregion

        #region Private Methods
        private void ListenForData(SocketAsyncEventArgs args)
        {
            lock (this)
            {
                Socket socket = (args.UserToken as State).socket;
                if (socket.Connected)
                {
                    socket.InvokeAsyncMethod(socket.ReceiveAsync,
                        ReceivedCompleted, args);
                }
            }
        }

        private void ReceivedCompleted(Object sender, SocketAsyncEventArgs args)
        {
            if (args.BytesTransferred == 0)
            {
                CloseConnection(args); //Graceful disconnect
                return;
            }
            if (args.SocketError != SocketError.Success)
            {
                CloseConnection(args); //NOT graceful disconnect
                return;
            }

            State state = args.UserToken as State;

            Byte[] data = new Byte[args.BytesTransferred];
            Array.Copy(args.Buffer, args.Offset, data, 0, data.Length);
            OnDataReceived(data, args.RemoteEndPoint as IPEndPoint, state.dataReceived);

            ListenForData(args);
        }

        private void CloseConnection(SocketAsyncEventArgs args)
        {
            State state = args.UserToken as State;
            Socket socket = state.socket;
            try
            {
                socket.Shutdown(SocketShutdown.Both);
            }
            catch { } 
            socket.Close();
            socket = null;

            args.Completed -= ReceivedCompleted;
            OnDisconnected(args, state.disconnectedCallback);
        }
        #endregion

        #region Events

        private void OnDataReceived(Byte[] data, IPEndPoint remoteEndPoint, DataReceivedCallback callback)
        {
            receivedPartialData.AddRange(data);

            while (true) 
            {
                int splitIndex = receivedPartialData.IndexOf((byte)0xFF);
                if (splitIndex == -1)
                {
                    break;
                }
                byte[] packet = receivedPartialData.Take(splitIndex).ToArray();
                receivedPartialData.RemoveRange(0, splitIndex + 1);
                callback(this, new DataEventArgs() { RemoteEndPoint = remoteEndPoint, Data = packet });
            }
        }

        private void OnDisconnected(SocketAsyncEventArgs args, DisconnectedCallback callback)
        {
            callback(this, args);
        }
        #endregion
    }
}